Skip to content

Dialog: dynamically switch footer button layout based on available height#7683

Open
liuliu-dev wants to merge 4 commits intomainfrom
liuliu/dynamically-switch-footer-button-layout
Open

Dialog: dynamically switch footer button layout based on available height#7683
liuliu-dev wants to merge 4 commits intomainfrom
liuliu/dynamically-switch-footer-button-layout

Conversation

@liuliu-dev
Copy link
Contributor

@liuliu-dev liuliu-dev commented Mar 19, 2026

Fixes an accessibility issue where Dialog footer buttons caused 2D scrolling at small viewport sizes (e.g. 320×256px).

Previously, the footer switched to horizontal scroll via a fixed @media (max-height: 325px) rule. Now the footer wraps (default) when there's enough room for body content (≥56px), and it scrolls horizontally when the viewport is too short to fit both wrapped footer and minimum body content.

Tested the fix in https://github.com/github/primer-docs/pull/977

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

Merge checklist

@changeset-bot
Copy link

changeset-bot bot commented Mar 19, 2026

🦋 Changeset detected

Latest commit: 2c08367

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Contributor

⚠️ Action required

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Check the integration testing docs for step-by-step instructions. Or, apply the integration-tests: skipped manually label to skip these checks.

@github-actions github-actions bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Mar 19, 2026
@github-actions github-actions bot temporarily deployed to storybook-preview-7683 March 19, 2026 18:37 Inactive
@primer-integration
Copy link

👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/16505

@primer-integration
Copy link

Integration test results from github/github-ui:

Passed  CI   Passed
Passed  VRT   Passed
Passed  Projects   Passed

All checks passed!

@liuliu-dev liuliu-dev requested a review from TylerJDev March 19, 2026 21:34
@liuliu-dev liuliu-dev marked this pull request as ready for review March 19, 2026 21:34
@liuliu-dev liuliu-dev requested a review from a team as a code owner March 19, 2026 21:34
Copilot AI review requested due to automatic review settings March 19, 2026 21:34
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Dialog footer button layout behavior to avoid 2D scrolling in very small viewports by dynamically choosing between wrapped buttons and horizontal scrolling based on available vertical space.

Changes:

  • Add runtime measurement logic (via useResizeObserver) to decide whether footer buttons should wrap or horizontally scroll.
  • Replace the previous fixed @media (max-height: 325px) footer rule with a data-footer-button-layout-driven CSS selector.
  • Add a patch changeset entry for the @primer/react release.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
packages/react/src/Dialog/Dialog.tsx Adds measurement + state to set data-footer-button-layout based on computed available body height.
packages/react/src/Dialog/Dialog.module.css Swaps the fixed viewport-height media query for an attribute-driven “scroll” footer layout rule.
.changeset/neat-moose-dress.md Declares a patch release note for the Dialog footer behavior change.

Comment on lines +399 to +408
const viewportHeight = backdropRef.current?.clientHeight ?? window.innerHeight
const positionRegular = dialogElement.getAttribute('data-position-regular')
const positionNarrow = dialogElement.getAttribute('data-position-narrow')
// fullscreen/left/right fill the full viewport; otherwise match CSS max-height gutter.
const gutter = viewportHeight <= 280 ? 12 : 64
const dialogMaxHeight =
positionNarrow === 'fullscreen' || positionRegular === 'left' || positionRegular === 'right'
? viewportHeight
: Math.max(0, viewportHeight - gutter)

Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gutter/dialogMaxHeight calculation re-implements CSS max-height logic, but it doesn’t match the stylesheet: 12px gutter is only applied under @media (max-width: 767px) { @media (max-height: 280px) { ... } } in Dialog.module.css, while this code applies 12 whenever viewportHeight <= 280 regardless of viewport width. This can cause the footer layout decision to diverge from the actual dialog max-height. Consider deriving the max height from computed styles (e.g. getComputedStyle(dialogElement).maxHeight) or mirroring the same width breakpoint check before switching to the 12px gutter.

Copilot uses AI. Check for mistakes.

setFooterButtonLayout(visibleBodyHeightWithWrap >= MIN_BODY_HEIGHT ? 'wrap' : 'scroll')
}, [hasFooter])

Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateFooterButtonLayout is only triggered via useResizeObserver(..., backdropRef), so the layout may become stale when footer/header content changes without a viewport/backdrop resize (e.g. button label changes, async loading states, toggling footerButtons). Additionally, in the useResizeObserver fallback path (no ResizeObserver), the callback isn’t invoked on mount—only on window.resize—so data-footer-button-layout may never be set correctly initially. Consider calling updateFooterButtonLayout() in an effect when relevant inputs change (and/or observing the dialog/footer element too) so the layout decision is updated even without a resize event.

Suggested change
useEffect(() => {
updateFooterButtonLayout()
}, [updateFooterButtonLayout])

Copilot uses AI. Check for mistakes.
Comment on lines +248 to +275
function measureWrappedFooterHeight(footerElement: HTMLElement) {
const measurementContainer = document.createElement('div')
const measuredFooter = footerElement.cloneNode(true) as HTMLElement

Object.assign(measurementContainer.style, {
position: 'fixed',
top: '0',
left: '-99999px',
visibility: 'hidden',
pointerEvents: 'none',
contain: 'layout style size',
})

measuredFooter.style.width = `${footerElement.getBoundingClientRect().width}px`

Object.assign(measuredFooter.style, {
flexWrap: 'wrap',
overflowX: '',
overflowY: '',
justifyContent: '',
})

measurementContainer.appendChild(measuredFooter)
document.body.appendChild(measurementContainer)

const measuredHeight = measuredFooter.offsetHeight
measurementContainer.remove()

Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

measureWrappedFooterHeight clones the entire footer, inserts it into document.body, forces layout, then removes it on every resize observer callback. This can be relatively expensive during viewport changes (especially on mobile where viewport height can change frequently). Consider caching/reusing a single measurement container and only re-measuring when width/footer content changes, to reduce DOM churn and forced reflow.

Copilot uses AI. Check for mistakes.
Comment on lines +450 to +451
data-has-footer={hasFooter ? '' : undefined}
data-footer-button-layout={hasFooter ? footerButtonLayout : undefined}
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR introduces a new data-footer-button-layout attribute that changes footer behavior based on measured heights, but Dialog.test.tsx doesn’t currently cover this new behavior. Adding a unit test that mocks ResizeObserver and element measurements (e.g. offsetHeight / getBoundingClientRect) would help prevent regressions for the wrap-vs-scroll switching logic.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants